#!/usr/bin/python3
# -*- coding:utf-8 -*-
#######################################################
# Author: liangliangSu
# Created Time: 2023-09-08 16:45
# Email: sll917@outlook.com
# Version: V1.0
# File Name: 17函数.py
#######################################################
print('(1)------------------完美分割线---------------------')
#函数是组织好的，可重复使用的，用来实现单一，或相关联功能的代码段。
#函数能提高应用的模块性，和代码的重复利用率。你已经知道Python提供了许多内建函数，比如print()。但你也可以自己创建函数，这被叫做用户自定义函数。
#定义一个函数
#你可以定义一个由自己想要功能的函数，以下是简单的规则：
#函数代码块以 def 关键词开头，后接函数标识符名称和圆括号 ()。
#任何传入参数和自变量必须放在圆括号中间，圆括号之间可以用于定义参数。
#函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
#函数内容以冒号 : 起始，并且缩进。
#return [表达式] 结束函数，选择性地返回一个值给调用方，不带表达式的 return 相当于返回 None。
#语法
#Python 定义函数使用 def 关键字，一般格式如下：
"""
def 函数名（参数列表）:
    函数体
"""

#默认情况下，参数值和参数名称是按函数声明中定义的顺序匹配起来的。
#让我们使用函数来输出"Hello World！"：
def hello() :
    print("Hello World!")
hello()
#更复杂点的应用，函数中带上参数变量:

#比较两个数，并返回较大的数:
print('(2)------------------完美分割线---------------------')
def max(a, b):
    if a > b:
        return a
    else:
        return b
a = 4
b = 5
print(max(a, b))

print('(3)------------------完美分割线---------------------')
# 计算面积函数
def area(width, height):
    return width * height

def print_welcome(name):
    print("Welcome", name)
print_welcome("python")

w = 4 
h = 5
print(" area =", area(w, h))

#函数调用
#定义一个函数：给了函数一个名称，指定了函数里包含的参数，和代码块结构。
#这个函数的基本结构完成以后，你可以通过另一个函数调用执行，也可以直接从 Python 命令提示符执行。
#如下实例调用了 printme() 函数：
print('(4)------------------完美分割线---------------------')
# 定义函数
def printme(str):
   # 打印任何传入的字符串
   print (str)
   return

# 调用函数
printme("我要调用用户自定义函数!")
printme("再次调用同一函数")

#参数传递
#在 python 中，类型属于对象，对象有不同类型的区分，变量是没有类型的：
#a=[1,2,3]
#a="Runoob"
#以上代码中，[1,2,3] 是 List 类型，"Runoob" 是 String 类型，而变量 a 是没有类型，她仅仅是一个对象的引用（一个指针），可以是指向 List 类型对象，也可以是指向 String 类型对象。

#可更改(mutable)与不可更改(immutable)对象
#在 python 中，strings, tuples, 和 numbers 是不可更改的对象，而 list,dict 等则是可以修改的对象。
#不可变类型：变量赋值 a=5 后再赋值 a=10，这里实际是新生成一个 int 值对象 10，再让 a 指向它，而 5 被丢弃，不是改变 a 的值，相当于新生成了 a。
#可变类型：变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改，本身la没有动，只是其内部的一部分值被修改了。

#python 函数的参数传递：
#不可变类型：类似 C++ 的值传递，如整数、字符串、元组。如 fun(a)，传递的只是 a 的值，没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值，则是新生成一个 a 的对象。
#可变类型：类似 C++ 的引用传递，如 列表，字典。如 fun(la)，则是将 la 真正的传过去，修改后 fun 外部的 la 也会受影响
#python 中一切都是对象，严格意义我们不能说值传递还是引用传递，我们应该说传不可变对象和传可变对象

#python 传不可变对象实例
#通过 id() 函数来查看内存地址变化：
print('(5)------------------完美分割线---------------------')
def change(a):
    print(id(a))   # 指向的是同一个对象
    a=10
    print(id(a))   # 一个新对象
a=1
print(id(a))
print('------------------完美分割线---------------------')
change(a)
#可以看见在调用函数前后，形参和实参指向的是同一个对象（对象 id 相同），
#在函数内部修改形参后，形参指向的是不同的 id。

#传可变对象实例
#可变对象在函数里修改了参数，那么在调用这个函数的函数里，原始的参数也被改变了。如：
print('(6)------------------完美分割线---------------------')
# 可写函数说明
def changeme( mylist ):
   "修改传入的列表"
   mylist.append([1,2,3,4])
   print ("函数内取值: ", mylist)
   return
# 调用changeme函数
mylist = [10,20,30]
changeme( mylist )
print ("函数外取值: ", mylist)
#传入函数的和在末尾添加新内容的对象用的是同一个引用。故输出结果如下：
#函数内取值:  [10, 20, 30, [1, 2, 3, 4]]
#函数外取值:  [10, 20, 30, [1, 2, 3, 4]]

"""
调用函数时可使用的正式参数类型：
必需参数
关键字参数
默认参数
不定长参数
"""
#1,必需参数
#必需参数须以正确的顺序传入函数。调用时的数量必须和声明时的一样。
#调用 printme() 函数，你必须传入一个参数，不然会出现语法错误：
print('(7)------------------完美分割线---------------------')

#def printme( str ):
#   "打印任何传入的字符串"
#   print (str)
#   return
## 调用 printme 函数，不加参数会报错
#printme()
#以上实例输出结果：
#Traceback (most recent call last):
#  File "test.py", line 10, in <module>
#    printme()
#TypeError: printme() missing 1 required positional argument: 'str'

#2,关键字参数
#关键字参数和函数调用关系紧密，函数调用使用关键字参数来确定传入的参数值。
#使用关键字参数允许函数调用时参数的顺序与声明时不一致，因为 Python 解释器能够用参数名匹配参数值。
#以下实例在函数 printme() 调用时使用参数名：
print('(8)------------------完美分割线---------------------')

def printstr( str ):
   "打印任何传入的字符串"
   print (str)
   return
#调用printme函数
printstr( str = "菜鸟教程")

#以下实例中演示了函数参数的使用不需要使用指定顺序：
print('(9)------------------完美分割线---------------------')
def printinfo0( name, age ):
   "打印任何传入的字符串"
   print ("名字: ", name)
   print ("年龄: ", age)
   return
#调用printinfo0函数
printinfo0( age=50, name="tom" )

#3,默认参数
#调用函数时，如果没有传递参数，则会使用默认参数。以下实例中如果没有传入 age 参数，则使用默认值：
print('(10)------------------完美分割线---------------------')
def printinfo1( name, age = 35 ):
   "打印任何传入的字符串"
   print ("名字: ", name)
   print ("年龄: ", age)
   return
#调用printinfo1函数
printinfo1( age=50, name="google" )
print(' ----------------------分割线--------------------------------')
printinfo1( name="coco" )

#4,不定长参数
#你可能需要一个函数能处理比当初声明时更多的参数。这些参数叫做不定长参数，和上述 2 种参数不同，声明时不会命名。基本语法如下：
"""
def functionname([formal_args,] *var_args_tuple ):
  "函数_文档字符串"
  function_suite
   return [expression]
"""
#加了星号 * 的参数会以元组(tuple)的形式导入，存放所有未命名的变量参数。
print('(11)------------------完美分割线---------------------')
def printinfo2( arg1, *vartuple ):
   "打印任何传入的参数"
   print (arg1)
   print (vartuple)
# 调用printinfo2 函数
printinfo2( 70, 60, 50 )

#如果在函数调用时没有指定参数，它就是一个空元组。我们也可以不向函数传递未命名的变量。如下实例：
print('(12)------------------完美分割线---------------------')
def printinfo3( arg1, *vartuple ):
   "打印任何传入的参数"
   print (arg1)
   for var in vartuple:
      print (var)
   return
# 调用printinfo3 函数
printinfo3( 10 )
printinfo3( 70, 60, 50 )

#还有一种就是参数带两个星号 **基本语法如下：
"""
def functionname([formal_args,] **var_args_dict ):
   "函数_文档字符串"
   function_suite
   return [expression]
"""
#加了两个星号 ** 的参数会以字典的形式导入。
print('(13)------------------完美分割线---------------------')
def printinfo4( arg1, **vardict ):
   "打印任何传入的参数"
   print (arg1)
   print (vardict)
# 调用printinfo4 函数
printinfo4(1, a=2,b=3)
#以上实例输出结果：
#1
#{'a': 2, 'b': 3}

#声明函数时，参数中星号 * 可以单独出现，例如:
#def f(a,b,*,c):
#    return a+b+c
#如果单独出现星号 *，则星号 * 后的参数必须用关键字传入：
# keyword.kwlist
#['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
##>>> def f(a,b,*,c):
#...     return a+b+c
#...
#>>> f(1,2,3)   # 报错
#Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
#TypeError: f() takes 2 positional arguments but 3 were given
#>>> f(1,2,c=3) # 正常

#匿名函数
"""
#Python 使用 lambda 来创建匿名函数。
#所谓匿名，意即不再使用 def 语句这样标准的形式定义一个函数。
#lambda 只是一个表达式，函数体比 def 简单很多。
#lambda 的主体是一个表达式，而不是一个代码块。仅仅能在 lambda 表达式中封装有限的逻辑进去。
#lambda 函数拥有自己的命名空间，且不能访问自己参数列表之外或全局命名空间里的参数。
#虽然 lambda 函数看起来只能写一行，却不等同于 C 或 C++ 的内联函数，后者的目的是调用小函数时不占用栈内存从而增加运行效率。
"""
#语法
#lambda 函数的语法只包含一个语句，如下：
"""
ambda [arg1 [,arg2,.....argn]]:expression
"""
#设置参数 a 加上 10:
print('(14)------------------完美分割线---------------------')
x = lambda a : a + 10
print(x(9))

print('(15)------------------完美分割线---------------------')
#以下实例匿名函数设置两个参数：
sum = lambda arg1, arg2: arg1 + arg2
pro = lambda arg1, arg2: arg1 * arg2
# 调用sum函数
print ("相加后的值为 : ", sum( 10, 20 ))
print ("相加后的值为 : ", sum( 20, 20 ))
# 调pro函数
print ("相乘后的值为 : ", pro( 20, 20 ))
print ("相乘后的值为 : ", pro( 20, 20 ))
#以上实例输出结果：
#相加后的值为 :  30
#相加后的值为 :  40

#我们可以将匿名函数封装在一个函数内，这样可以使用同样的代码来创建多个匿名函数。
#以下实例将匿名函数封装在 myfunc 函数中，通过传入不同的参数来创建不同的匿名函数：
print('(16)------------------完美分割线---------------------')
def myfunc(n):
    return lambda a : a * n
mydoubler = myfunc(2)
mytripler = myfunc(3)
print(mydoubler(11))
print(mytripler(11))
#以上实例输出结果：
#22
#33

#return 语句
#return [表达式] 语句用于退出函数，选择性地向调用方返回一个表达式。不带参数值的 return 语句返回 None。之前的例子都没有示范如何返回数值，以下实例演示了 return 语句的用法：
print('(17)------------------完美分割线---------------------')
def sum2( arg1, arg2 ):
   # 返回2个参数的和."
   total = arg1 + arg2
   print ("函数内 : ", total)
   return total
# 调用sum2函数
total = sum2( 10, 20 )
print ("函数外 : ", total)
#以上实例输出结果：
#函数内 :  30
#函数外 :  30

#强制位置参数
#Python3.8 新增了一个函数形参语法 / 用来指明函数形参必须使用指定位置参数，不能使用关键字参数的形式。
#在以下的例子中，形参 a 和 b 必须使用指定位置参数，c 或 d 可以是位置形参或关键字形参，而 e 和 f 要求为关键字形参:
print(' ----------------------分割线--------------------------------')
def f(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)
#以下使用方法是正确的:
f(10, 20, 30, d=40, e=50, f=60)
f(10, 20, 30, 40, e=50, f=60)
f(10, 20, c=30,d=40, e=50, f=60)
#以下使用方法会发生错误:
#f(10, b=20, c=30, d=40, e=50, f=60)   # b 不能使用关键字参数的形式
#f(10, 20, 30, 40, 50, f=60)           # e 必须使用关键字参数的形式

#!/usr/bin/python3
# -*- coding:utf-8 -*-
#######################################################
# Author: liangliangSu
# Created Time: 2023-09-09 10:03
# Email: sll917@outlook.com
# Version: V1.0
# File Name: 17函数-2.py
#######################################################
# 函数是可重用的程序代码块。函数的作用，不仅可以实现代码的复用，更能实现代码的
# 一致性。一致性指的是，只要修改函数的代码，则所有调用该函数的地方都能得到体现。
# 在编写函数时，函数体中的代码写法和我们前面讲述的基本一致，只是对代码实现了封
# 装，并增加了函数调用、传递参数、返回计算结果等内容。
# 为了让大家更容易理解，掌握的更深刻。我们也要深入内存底层进行分析。绝大多数语
# 言内存底层都是高度相似的，这样大家掌握了这些内容也便于以后学习其他语言.
# Python 函数的分类
# Python 中函数分为如下几类
"""
1. 内置函数
我们前面使用的str()、list()、len()等这些都是内置函数，我们可以拿来直接使用。
2. 标准库函数
我们可以通过import 语句导入库，然后使用其中定义的函数
3. 第三方库函数
Python 社区也提供了很多高质量的库。下载安装这些库后，也是通过import 语句导
入，然后可以使用这些第三方库的函数
4. 用户自定义函数
用户自己定义的函数，显然也是开发中适应用户自身需求定义的函数。今天我们学习的
就是如何自定义函数。
"""
# 函数的定义和调用
# 核心要点
# Python 中，定义函数的语法如下：
"""
def 函数名([参数列表]) :
    '''文档字符串'''
    函数体/若干语句
"""

# 要点：
# 1. 我们使用def 来定义函数，然后就是一个空格和函数名称；
# (1) Python 执行def 时，会创建一个函数对象，并绑定到函数名变量上。
# 2. 参数列表
# (1) 圆括号内是形式参数列表，有多个参数则使用逗号隔开
# (2) 形式参数不需要声明类型，也不需要指定函数返回值类型
# (3) 无参数，也必须保留空的圆括号
# (4) 实参列表必须与形参列表一一对应
# 3. return 返回值
# (1) 如果函数体中包含return 语句，则结束函数执行并返回值；
# (2) 如果函数体中不包含return 语句，则返回None 值.
# 4. 调用函数之前，必须要先定义函数，即先调用def 创建函数对象
# (1) 内置函数对象会自动创建
# (2) 标准库和第三方库函数，通过import 导入模块时，会执行模块中的def 语句
# 我们通过实际定义函数来学习函数的定义方式.

# 形参和实参
# 形参和实参的要点，请参考上一节中的总结。在此不再赘述。
# 【操作】定义一个函数，实现两个数的比较，并返回较大的值。
def PrintMax(a1, b1):  # 实现两个数的比较，并返回较大的值
    if a1 > b1:
        print(a1, '较大值')
    else:
        print(b1, '较大值')


PrintMax(10, 20)
PrintMax(30, 5)
print('1）************************************')
"""
上面的printMax 函数中，在定义时写的printMax(a,b)。a 和b 称为“形式参数”，
简称“形参”。也就是说，形式参数是在定义函数时使用的。
形式参数的命名只要符合“标识符”命名规则即可。
在调用函数时，传递的参数称为“实际参数”，简称“实参”。上面代码中，
printMax(10,20)，10 和20 就是实际参数。
"""

# 文档字符串(函数的注释)
# 程序的可读性最重要，一般建议在函数体开始的部分附上函数定义说明，这就是“文档字符
# 串”，也有人成为“函数的注释”。我们通过三个单引号或者三个双引号来实现，中间可以
# 加入多行文字进行说明。
# 【操作】测试文档字符串的使用
# def print_star(n):
# '''根据传入的n，打印多个星号'''
# print("*"*n)
# help(print_star)
# 我们调用help(函数名.__doc__)可以打印输出函数的文档字符串。执行结果如下：
# Help on function print_star in module __main__:
# print_star(n)
# 根据传入的n，打印多个星号

# 返回值
# return 返回值要点：
# 1. 如果函数体中包含return 语句，则结束函数执行并返回值；
# 2. 如果函数体中不包含return 语句，则返回None 值。
# 3. 要返回多个返回值，使用列表、元组、字典、集合将多个值“存起来”即可。
# 【操作】定义一个打印n 个星号的无返回值的函数
def print_star(n):
    print("*" * n)

print_star(5)

# 【操作】定义一个返回两个数平均值的函数
def myavg(x1, y1):
    return (x1 + y1) / 2

c = myavg(20, 30)
print(c)

# 函数也是对象，内存底层分析
# Python 中，“一切都是对象”。实际上，执行def 定义函数后，系统就创建了相应的函数
# 对象。我们执行如下程序，然后进行解释：
def print_star(n):
    print("*" * n)

print(id(print_star))
print_star(3)

# 上面代码执行def 时，系统中会创建函数对象，并通过print_star 这个变量进行引用：
# 我们执行“c=print_star”后，显然将print_star 变量的值赋给了变量c，内存图变成了：
# 显然，我们可以看出变量c 和print_star 都是指向了同一个函数对象。因此，执行c(3)和执
# 行print_star(3)的效果是完全一致的。Python 中，圆括号意味着调用函数。在没有圆括
# 号的情况下，Python 会把函数当做普通对象。
# 与此核心原理类似，我们也可以做如下操作：
# zhengshu = int
# zhengshu("234")
# 显然，我们将内置函数对象int()赋值给了变量zhengshu，这样zhengshu 和int 都是指向
# 了同一个内置函数对象。当然，此处仅限于原理性讲解，实际开发中没必要这么做。

# 变量的作用域(全局变量和局部变量)
# 变量起作用的范围称为变量的作用域，不同作用域内同名变量之间互不影响。变量分为：全
# 局变量、局部变量。
# 全局变量：
# 1. 在函数和类定义之外声明的变量。作用域为定义的模块，从定义位置开始直到模块结束。
# 2. 全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
# 3. 全局变量一般做常量使用。
# 4. 函数内要改变全局变量的值，使用global 声明一下
# 局部变量：
# 1. 在函数体中（包含形式参数）声明的变量。
# 2. 局部变量的引用比全局变量快，优先考虑使用。
# 3. 如果局部变量和全局变量同名，则在函数内隐藏全局变量，只使用同名的局部变量
# 【操作】全局变量的作用域测试
print('2）************************************')
a = 100  # 全局变量

def f1():
    global a  # 如果要在函数内改变全局变量的值，增加global 关键字声明
    print(a)  # 打印全局变量a 的值
    a = 300

f1()
print('************************************')
print(a)

# 【操作】全局变量和局部变量同名测试
print('3）************************************')
a = 100

def f1():
    a = 3  # 同名的局部变量
    print(a)

f1()
print(a)  # a 仍然是100，没有变化
print('4）************************************')

# 【操作】输出局部变量和全局变量
a = 100

def f1(a, b, c):
    print(a, b, c)
    print(locals())  # 打印输出的局部变量
    print('************************************')
    print(globals())  # 打印输出的全局变量

f1(2, 3, 4)

# 局部变量和全局变量效率测试
# 局部变量的查询和访问速度比全局变量快，优先考虑使用，尤其是在循环的时候。
# 在特别强调效率的地方或者循环次数较多的地方，可以通过将全局变量转为局部变量提高运行速度。
# 【操作】测试局部变量和全局变量效率
# 测试局部变量、全局变量的效率
print('5）************************************')
import math
import time

def test01():
     start = time.time()
     for j in range(10000000):
         math.sqrt(30)
     end = time.time()
     print("耗时方案1：{0}".format((end - start)))

def test02():
     b = math.sqrt
     start = time.time()
     for i in range(10000000):
         b(30)
     end = time.time()
     print("耗时方案2：{0}".format((end - start)))

test01()
test02()
print('6）************************************')

# 参数的传递
# 函数的参数传递本质上就是：从实参到形参的赋值操作。Python 中“一切皆对象”，
# 所有的赋值操作都是“引用的赋值”。所以，Python 中参数的传递都是“引用传递”，不
# 是“值传递”。具体操作时分为两类：
# 1. 对“可变对象”进行“写操作”，直接作用于原对象本身。
# 2. 对“不可变对象”进行“写操作”，会产生一个新的“对象空间”，并用新的值填
# 充这块空间。（起到其他语言的“值传递”效果，但不是“值传递”)
"""
可变对象有：
字典、列表、集合、自定义的对象等
不可变对象有：
数字、字符串、元组、function 等
# """

# 传递可变对象的引用
# 传递参数是可变对象（例如：列表、字典、自定义的其他可变对象等），实际传递的还是对
# 象的引用。在函数体中不创建新的对象拷贝，而是可以直接修改所传递的对象。
# 【操作】参数传递：传递可变对象的引用
b = [10, 20]

def f2(m):
    print("m:", id(m))  # b 和m 是同一个对象
    m.append(30)  # 由于m 是可变对象，不创建对象拷贝，直接修改这个对象

f2(b)
print("b:", id(b))
print(b)

# 传递不可变对象的引用
# 传递参数是不可变对象（例如：int、float、字符串、元组、布尔值），实际传递的还是对
# 象的引用。在”赋值操作”时，由于不可变对象无法修改，系统会新创建一个对象。
# 【操作】参数传递：传递不可变对象的引用
print('7）************************************')
x = 100

def f1(n):
    print("n:", id(n))  # 传递进来的是a 对象的地址
    n = n + 200  # 由于a 是不可变对象，因此创建新的对象n
    print("n:", id(n))  # n 已经变成了新的对象
    print(n)

f1(x)
print("a:", id(x))

# 执行结果：
# n: 1663816464
# n: 46608592
# 300
# a: 1663816464
# 显然，通过id 值我们可以看到n 和a 一开始是同一个对象。给n 赋值后，n 是新的对象.

# 浅拷贝和深拷贝
# 为了更深入的了解参数传递的底层原理，我们需要讲解一下“浅拷贝和深拷贝”。我们可以
# 使用内置函数：copy(浅拷贝)、deepcopy(深拷贝)。
# 浅拷贝：不拷贝子对象的内容，只是拷贝子对象的引用。
# 深拷贝：会连子对象的内存也全部拷贝一份，对子对象的修改不会影响源对象
# 测试浅拷贝和深拷贝
print('8）************************************')
import copy

def TestCopy():
    # 测试浅拷贝
    a = [10, 20, [5, 6]]
    b = copy.copy(a)
    print("a", a)
    print("b", b)
    b.append(30)
    b[2].append(7)
    print("浅拷贝......")
    print("a", a)
    print("b", b)


def TestDeepCopy():
    # 测试深拷贝
    a = [10, 20, [5, 6]]
    b = copy.deepcopy(a)
    print("a", a)
    print("b", b)
    b.append(30)
    b[2].append(7)
    print("深拷贝......")
    print("a", a)
    print("b", b)

TestCopy()
print('9）************************************')
TestDeepCopy()


# 参数的几种类型
# 位置参数
# 函数调用时，实参默认按位置顺序传递，需要个数和形参匹配。按位置传递的参数，称为：“位置参数”。
print('10）************************************')
# 【操作】测试位置参数
def f1(a, b, c):
    print(a, b, c)

f1(2, 3, 4)
# f1(2,3) #报错，位置参数不匹配

# 默认值参数
# 我们可以为某些参数设置默认值，这样这些参数在传递时就是可选的。称为“默认值参数”。
# 默认值参数放到位置参数后面。
# 【操作】测试默认值参数
print('11）************************************')
def f1(a, b, c=10, d=20):  # 默认值参数必须位于普通位置参数后面
    print(a, b, c, d)

f1(8, 9)
f1(8, 9, 19)
f1(8, 9, 19, 29)
# 执行结果：
# 8 9 10 20
# 8 9 19 20
# 8 9 19 29

# 命名参数
# 我们也可以按照形参的名称传递参数，称为“命名参数”，也称“关键字参数”。
# 【操作】测试命名参数
print('12）************************************')
def f1(a, b, c):
    print(a, b, c)

f1(8, 9, 19)  # 位置参数
f1(c=10, a=20, b=30)  # 命名参数
# 执行结果：
# 8 9 19
# 20 30 10

"""
可变参数
可变参数指的是“可变数量的参数”。分两种情况：
1. *param（一个星号），将多个参数收集到一个“元组”对象中。
2. **param（两个星号），将多个参数收集到一个“字典”对象中。
"""

print('13）************************************')
# 【操作】测试可变参数处理（元组、字典两种方式）
def f1(a, b, *c):
    print(a, b, c)

f1(8, 9, 19, 20)
print('************************************')

def f2(a, b, **c):
    print(a, b, c)

f2(8, 9, name='gaoqi', age=18)

print('14）************************************')
def f3(a, b, *c, **d):
    print(a, b, c, d)

f3(8, 9, 20, 30, name='gaoqi', age=18)
# 执行结果：
# 8 9 (19, 20)
# 8 9 {'name': 'gaoqi', 'age': 18}
# 8 9 (20, 30) {'name': 'gaoqi', 'age': 18}

# 强制命名参数
# 在带星号的“可变参数”后面增加新的参数，必须在调用的时候“强制命名参数”。
# 【操作】强制命名参数的使用
print('15）************************************')
def f1(*a, b, c):
    print(a, b, c)

# f1(2,3,4) #会报错。由于a 是可变参数，将2,3,4 全部收集。造成b 和c 没有赋值。
f1(2, b=3, c=4)
# 执行结果：
# (2,) 3 4

# lambda 表达式和匿名函数
# lambda 表达式可以用来声明匿名函数。lambda 函数是一种简单的、在同一行中定义函数
# 的方法。lambda 函数实际生成了一个函数对象。
# lambda 表达式只允许包含一个表达式，不能包含复杂语句，该表达式的计算结果就是函数
# 的返回值.
"""
lambda 表达式的基本语法如下：
lambda arg1,arg2,arg3... : <表达式>
arg1/arg2/arg3 为函数的参数。<表达式>相当于函数体。运算结果是：表达式的运算结果。
"""
# 【操作】lambda 表达式使用
print('16）************************************')
f = lambda a, b, c: a + b + c
print(f)
print(f(2, 3, 4))
g = [lambda a: a * 2, lambda b: b * 3, lambda c: c * 4]
print(g[0](6), g[1](7), g[2](8))
print('17）************************************')
# 执行结果：
# <function <lambda> at 0x0000000002BB8620>
# 9
# 12 21 32

# eval()函数
# 功能：将字符串str 当成有效的表达式来求值并返回计算结果。
# 语法： eval(source[, globals[, locals]]) -> value
# 参数：
# source：一个Python 表达式或函数compile()返回的代码对象
# globals：可选。必须是dictionary
# locals：可选。任意映射对象
# #测试eval()函数
s = "print('abcde')"
eval(s)
x = 10
b = 20
c = eval("x+b")
print(c)
dict1 = dict(x=100, b=200)
d = eval("x+b", dict1)
print(d)
print('18）************************************')
# eval 函数会将字符串当做语句来执行，因此会被注入安全隐患。比如：字符串中含有删除文
# 件的语句。那就麻烦大了。因此，使用时候，要慎重!!!

# 递归函数
# 递归函数指的是：自己调用自己的函数，在函数体内部直接或间接的自己调用自己。递归类
# 似于大家中学数学学习过的“数学归纳法”.
"""
每个递归函数必须包含两个部分：
1. 终止条件
表示递归什么时候结束。一般用于返回值，不再调用自己。
2. 递归步骤
把第n 步的值和第n-1 步相关联。
递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力。在处理大量数据时，谨慎使用。
"""


# 【操作】使用递归函数计算阶乘(factorial)
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n - 1)


for i in range(1, 6):
    print(i, '!=', factorial(i))
print('19）************************************')

# 执行结果：
# # 1 != 1
# 2 != 2
# 3 != 6
# 4 != 24
# 5！=120

# 嵌套函数(内部函数)
# 嵌套函数：
# 在函数内部定义的函数！
# 【操作】嵌套函数定义
def f1():
    print('f1 running...')

    def f2():
        print('f2 running...')

    f2()


f1()
print('20）************************************')


# 执行结果：
# f1 running...
# f2 running...
# 上面程序中，f2()就是定义在f1 函数内部的函数。f2()的定义和调用都在f1()函数内部。

# 一般在什么情况下使用嵌套函数？
# 1. 封装- 数据隐藏
# 外部无法访问“嵌套函数”。
# 2. 贯彻DRY(Don’t Repeat Yourself) 原则
# 嵌套函数，可以让我们在函数内部避免重复代码。
# 3. 闭包
# 后面会详细讲解。
# 【操作】使用嵌套函数避免重复代码
def printChineseName(name, familyName):
    print("{0} {1}".format(familyName, name))


def printEnglishName(name, familyName):
    print("{0} {1}".format(name, familyName))


# 使用1 个函数代替上面的两个函数
def printName(isChinese, name, familyName):
    def inner_print(a, b):
        print("{0} {1}".format(a, b))

    if isChinese:
        inner_print(familyName, name)
    else:
        inner_print(name, familyName)


printName(True, "小七", "高")
printName(False, "George", "Bush")
print('21）************************************')

# nonlocal 关键字
# nonlocal 用来声明外层的局部变量。
# global 用来声明全局变量。
# 【操作】使用nonlocal 声明外层局部变量
# #测试nonlocal、global 关键字的用法
a2 = 100


def outer():
    b2 = 10

    def inner():
        nonlocal b2  # 声明外部函数的局部变量
        print("inner b2:", b2)
        b2 = 20
        global a2  # 声明全局变量
        a2 = 1000

    inner()
    print("outer b2:", b2)


outer()
print("a2：", a2)
print('22）************************************')
# LEGB 规则
# Python 在查找“名称”时，是按照LEGB 规则查找的：
#       Local-->Enclosed-->Global-->Built in
# Local 指的就是函数或者类的方法内部
# Enclosed 指的是嵌套函数（一个函数包裹另一个函数，闭包）
# Global 指的是模块中的全局变量
# Built in 指的是Python 为自己保留的特殊名称。
# 如果某个name 映射在局部(local)命名空间中没有找到，接下来就会在闭包作用域
# (enclosed)进行搜索，如果闭包作用域也没有找到，Python 就会到全局(global)命名空
# 间中进行查找，最后会在内建(built-in)命名空间搜索（如果一个名称在所有命名空间
# 中都没有找到，就会产生一个NameError）。
# #测试LEGB
# 测试LEGB
str = "global"


def outer():
    str = "outer"

    def inner():
        str = "inner"
        print(str)

    inner()


outer()
# 我们依次将几个str 注释掉，观察控制台打印的内容，体会LEBG 的搜索顺序。

# DocStrings
# Python 有一个甚是优美的功能称作文档字符串（Documentation Strings），在称呼它时通常
# 会使用另一个短一些的名字docstrings。DocStrings 是一款你应当使用的重要工具，它能够帮
# 助你更好地记录程序并让其更加易于理解。令人惊叹的是，当程序实际运行时，我们甚至可
# 以通过一个函数来获取文档！
# 案例（保存为 function_docstring.py ）：
print('23）************************************')
def print_max(x, y):
    '''Prints the maximum of two numbers.打印两个数值中的最大数。
    The two values must be integers.这两个数都应该是整数'''
    # 如果可能，将其转换至整数类型
    x = int(x)
    y = int(y)
    if x > y:
        print(x, 'is maximum')
    else:
        print(y, 'is maximum')
print_max(3, 5)
print(print_max.__doc__)
help(print_max)
